package no.nordicsemi.support.v18.scanner;

import ohos.bluetooth.ble.BlePeripheralDevice;
import ohos.interwork.utils.ParcelableEx;
import ohos.utils.Parcel;
import ohos.utils.SequenceUuid;
import ohos.utils.Sequenceable;
import org.jetbrains.annotations.Nullable;

import java.util.Arrays;
import java.util.List;
import java.util.UUID;

public final class ScanFilter implements Sequenceable {

    @Nullable
    private final String deviceName;

    @Nullable
    private final String deviceAddress;

    private final Object serviceUuid;
    private final Object serviceUuidMask;
    private final Object serviceDataUuid;

    @Nullable
    private final String serviceData;

    @Nullable
    private final String serviceDataMask;
    private final int manufacturerId;

    @Nullable
    private final String manufacturerData;

    @Nullable
    private final String manufacturerDataMask;
    private static final ScanFilter EMPTY = new Builder().build();

    private ScanFilter(@Nullable final String name, @Nullable final String deviceAddress,
                       final Object uuid, final Object uuidMask,
                       final Object serviceDataUuid, @Nullable final String serviceData,
                       @Nullable final String serviceDataMask, final int manufacturerId,
                       @Nullable final String manufacturerData,
                       @Nullable final String manufacturerDataMask) {
        this.deviceName = name;
        this.serviceUuid = uuid;
        this.serviceUuidMask = uuidMask;
        this.deviceAddress = deviceAddress;
        this.serviceDataUuid = serviceDataUuid;
        this.serviceData = serviceData;
        this.serviceDataMask = serviceDataMask;
        this.manufacturerId = manufacturerId;
        this.manufacturerData = manufacturerData;
        this.manufacturerDataMask = manufacturerDataMask;
    }

    public int describeContents() {
        return 0;
    }

    public void writeToParcel(final Parcel dest, final int flags) {
        dest.writeInt(deviceName == null ? 0 : 1);
        if (deviceName != null) {
            dest.writeString(deviceName);
        }
        dest.writeInt(deviceAddress == null ? 0 : 1);
        if (deviceAddress != null) {
            dest.writeString(deviceAddress);
        }
        dest.writeInt(serviceUuid == null ? 0 : 1);
        if (serviceUuid != null) {
            dest.writeParcelableEx((ParcelableEx) serviceUuid);
            dest.writeInt(serviceUuidMask == null ? 0 : 1);
            if (serviceUuidMask != null) {
                dest.writeParcelableEx((ParcelableEx) serviceUuidMask);
            }
        }
        dest.writeInt(serviceDataUuid == null ? 0 : 1);
        if (serviceDataUuid != null) {
            dest.writeParcelableEx((ParcelableEx) serviceUuid);
            dest.writeInt(serviceData == null ? 0 : 1);
            if (serviceData != null) {
                dest.writeInt(serviceData.length());
                dest.writeByteArray(serviceData.getBytes());

                dest.writeInt(serviceDataMask == null ? 0 : 1);
                if (serviceDataMask != null) {
                    dest.writeInt(serviceDataMask.length());
                    dest.writeByteArray(serviceDataMask.getBytes());
                }
            }
        }
        dest.writeInt(manufacturerId);
        dest.writeInt(manufacturerData == null ? 0 : 1);
        if (manufacturerData != null) {
            dest.writeInt(manufacturerData.length());
            dest.writeByteArray(manufacturerData.getBytes());

            dest.writeInt(manufacturerDataMask == null ? 0 : 1);
            if (manufacturerDataMask != null) {
                dest.writeInt(manufacturerDataMask.length());
                dest.writeByteArray(manufacturerDataMask.getBytes());
            }
        }
    }

    public static final Sequenceable.Producer<ScanFilter> CREATOR = new Producer<ScanFilter>() {
        public ScanFilter[] newArray(final int size) {
            return new ScanFilter[size];
        }

        public ScanFilter createFromParcel(final Parcel in) {
            final Builder builder = new Builder();
            if (in.readInt() == 1) {
                builder.setDeviceName(in.readString());
            }
            if (in.readInt() == 1) {
                builder.setDeviceAddress(in.readString());
            }
            if (in.readInt() == 1) {
                ParcelableEx uuid = in.readParcelableEx(UUID.class.getClassLoader());
                builder.setServiceUuid(uuid);
                if (in.readInt() == 1) {
                    ParcelableEx uuidMask = in.readParcelableEx(SequenceUuid.class.getClassLoader());
                    builder.setServiceUuid(uuid, uuidMask);
                }
            }
            if (in.readInt() == 1) {
                ParcelableEx serviceDataUuid = in.readParcelableEx(SequenceUuid.class.getClassLoader());
                if (in.readInt() == 1) {
                    final int serviceDataLength = in.readInt();
                    final byte[] serviceData = new byte[serviceDataLength];
                    in.readByteArray(serviceData);
                    if (in.readInt() == 0) {
                        builder.setServiceData(serviceDataUuid, Arrays.toString(serviceData));
                    } else {
                        final int serviceDataMaskLength = in.readInt();
                        final byte[] serviceDataMask = new byte[serviceDataMaskLength];
                        in.readByteArray(serviceDataMask);
                        builder.setServiceData(serviceDataUuid, Arrays.toString(serviceData), Arrays.toString(serviceDataMask));
                    }
                }
            }

            final int manufacturerId = in.readInt();
            if (in.readInt() == 1) {
                final int manufacturerDataLength = in.readInt();
                final byte[] manufacturerData = new byte[manufacturerDataLength];
                in.readByteArray(manufacturerData);
                if (in.readInt() == 0) {
                    builder.setManufacturerData(manufacturerId, Arrays.toString(manufacturerData));
                } else {
                    final int manufacturerDataMaskLength = in.readInt();
                    final byte[] manufacturerDataMask = new byte[manufacturerDataMaskLength];
                    in.readByteArray(manufacturerDataMask);
                    builder.setManufacturerData(manufacturerId, Arrays.toString(manufacturerData),
                            Arrays.toString(manufacturerDataMask));
                }
            }
            return builder.build();
        }
    };

    @Nullable
    public String getDeviceName() {
        return deviceName;
    }

    public Object getServiceUuid() {
        return serviceUuid;
    }

    public Object getServiceUuidMask() {
        return serviceUuidMask;
    }

    @Nullable
    public String getDeviceAddress() {
        return deviceAddress;
    }

    @Nullable
    public byte[] getServiceData() {
        return serviceData.getBytes();
    }

    @Nullable
    public byte[] getServiceDataMask() {
        return serviceDataMask.getBytes();
    }

    public Object getServiceDataUuid() {
        return serviceDataUuid;
    }

    public int getManufacturerId() {
        return manufacturerId;
    }

    @Nullable
    public byte[] getManufacturerData() {
        return manufacturerData.getBytes();
    }

    @Nullable
    public byte[] getManufacturerDataMask() {
        return manufacturerDataMask.getBytes();
    }

    public boolean matches(final ScanResult scanResult) {
        if (scanResult == null) {
            return false;
        }
        BlePeripheralDevice device = (BlePeripheralDevice) scanResult.getDevice();
        if (deviceAddress != null && !deviceAddress.equals(device.getDeviceAddr())) {
            return false;
        }
        final BlePeripheralDevice scanRecord = (BlePeripheralDevice) scanResult.getDevice();
        if (scanRecord == null
                && (deviceName != null || serviceUuid != null || manufacturerData != null
                || serviceData != null)) {
            return false;
        }
        if (deviceName != null) {
            scanRecord.getDeviceName();
            return false;
        }
        return true;
    }

    private static boolean matchesServiceUuids(final SequenceUuid uuid,final SequenceUuid parcelUuidMask,
                                               @Nullable final List<SequenceUuid> uuids) {
        if (uuid == null) {
            return true;
        }
        if (uuids == null) {
            return false;
        }

        for (final SequenceUuid parcelUuid : uuids) {
            final UUID uuidMask = parcelUuidMask == null ? null : parcelUuidMask.getUuid();
            if (matchesServiceUuid(uuid.getUuid(), uuidMask, parcelUuid.getUuid())) {
                return true;
            }
        }
        return false;
    }
    private
    static boolean matchesServiceUuid(final UUID uuid,
                                              @Nullable final UUID mask,
                                              final UUID data) {
        if (mask == null) {
            return uuid.equals(data);
        }
        if ((uuid.getLeastSignificantBits() & mask.getLeastSignificantBits())
                != (data.getLeastSignificantBits() & mask.getLeastSignificantBits())) {
            return false;
        }
        return ((uuid.getMostSignificantBits() & mask.getMostSignificantBits())
                == (data.getMostSignificantBits() & mask.getMostSignificantBits()));
    }
    private boolean matchesPartialData(@Nullable final byte[] data,
                                       @Nullable final byte[] dataMask,
                                       @Nullable final byte[] parsedData) {
        if (data == null) {
            return parsedData != null;
        }
        if (parsedData == null || parsedData.length < data.length) {
            return false;
        }
        if (dataMask == null) {
            for (int i = 0; i < data.length; ++i) {
                if (parsedData[i] != data[i]) {
                    return false;
                }
            }
            return true;
        }
        for (int i = 0; i < data.length; ++i) {
            if ((dataMask[i] & parsedData[i]) != (dataMask[i] & data[i])) {
                return false;
            }
        }
        return true;
    }

    @Override
    public String toString() {
        return "BluetoothLeScanFilter [deviceName=" + deviceName + ", deviceAddress="
                + deviceAddress
                + ", mUuid=" + serviceUuid + ", uuidMask=" + serviceUuidMask
                + ", serviceDataUuid=" + Objects.toString(serviceDataUuid) + ", serviceData="
                + serviceData + ", serviceDataMask="
                + serviceDataMask + ", manufacturerId=" + manufacturerId
                + ", manufacturerData=" + manufacturerData
                + ", manufacturerDataMask=" + manufacturerDataMask + "]";
    }

    @Override
    public int hashCode() {
        return Objects.hash(deviceName, deviceAddress, manufacturerId,
                manufacturerData,
                manufacturerDataMask,
                serviceData,
                serviceDataMask,
                serviceDataUuid,
                serviceUuid, serviceUuidMask);
    }

    @Override
    public boolean equals(final Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }
        final ScanFilter other = (ScanFilter) obj;
        return Objects.equals(deviceName, other.deviceName)
                && Objects.equals(deviceAddress, other.deviceAddress)
                && manufacturerId == other.manufacturerId
                && Objects.deepEquals(manufacturerData, other.manufacturerData)
                && Objects.deepEquals(manufacturerDataMask, other.manufacturerDataMask)
                && Objects.equals(serviceDataUuid, other.serviceDataUuid)
                && Objects.deepEquals(serviceData, other.serviceData)
                && Objects.deepEquals(serviceDataMask, other.serviceDataMask)
                && Objects.equals(serviceUuid, other.serviceUuid)
                && Objects.equals(serviceUuidMask, other.serviceUuidMask);
    }
    /* package */ boolean isAllFieldsEmpty() {
        return EMPTY.equals(this);
    }

    @Override
    public boolean hasFileDescriptor() {
        return false;
    }

    @Override
    public boolean marshalling(Parcel parcel) {
        return false;
    }

    @Override
    public boolean unmarshalling(Parcel parcel) {
        return false;
    }
    public static final class Builder {
        private String deviceName;
        private String deviceAddress;
        private Object serviceUuid;
        private Object uuidMask;
        private Object serviceDataUuid;
        private String serviceData;
        private String serviceDataMask;
        private int manufacturerId = -1;
        private String manufacturerData;
        private String manufacturerDataMask;

        public Builder setDeviceName(@Nullable final String deviceName) {
            this.deviceName = deviceName;
            return this;
        }

        public Builder setDeviceAddress(@Nullable final String deviceAddress) {
            this.deviceAddress = deviceAddress;
            return this;
        }

        public Builder setServiceUuid(final Object serviceUuid) {
            this.serviceUuid = serviceUuid;
            this.uuidMask = null; // clear uuid mask
            return this;
        }

        public Builder setServiceUuid(final Object serviceUuid,
                                      final Object uuidMask) {
            if (uuidMask != null && serviceUuid == null) {
                throw new IllegalArgumentException("uuid is null while uuidMask is not null!");
            }
            this.serviceUuid = serviceUuid;
            this.uuidMask = uuidMask;
            return this;
        }
        public Builder setServiceData(final Object serviceDataUuid,
                                      @Nullable final String serviceData) {
            if (serviceDataUuid == null) {
                throw new IllegalArgumentException("serviceDataUuid is null!");
            }
            this.serviceDataUuid = serviceDataUuid;
            this.serviceData = serviceData;
            this.serviceDataMask = null;
            return this;
        }

        public Builder setServiceData(final Object serviceDataUuid,
                                      @Nullable final String serviceData,
                                      @Nullable final String serviceDataMask) {
            if (serviceDataUuid == null) {
                throw new IllegalArgumentException("serviceDataUuid is null");
            }
            if (serviceDataMask != null) {
                if (serviceData == null) {
                    throw new IllegalArgumentException(
                            "serviceData is null while serviceDataMask is not null");
                }
                if (serviceData.length() != serviceDataMask.length()) {
                    throw new IllegalArgumentException(
                            "size mismatch for service data and service data mask");
                }
            }
            this.serviceDataUuid = serviceDataUuid;
            this.serviceData = serviceData;
            this.serviceDataMask = serviceDataMask;
            return this;
        }

        public Builder setManufacturerData(final int manufacturerId,
                                           @Nullable final String manufacturerData) {
            if (manufacturerData != null && manufacturerId < 0) {
                throw new IllegalArgumentException("invalid manufacture id");
            }
            this.manufacturerId = manufacturerId;
            this.manufacturerData = manufacturerData;
            this.manufacturerDataMask = null;
            return this;
        }

        public Builder setManufacturerData(final int manufacturerId,
                                           @Nullable final String manufacturerData,
                                           @Nullable final String manufacturerDataMask) {
            if (manufacturerData != null && manufacturerId < 0) {
                throw new IllegalArgumentException("invalid manufacture id");
            }
            if (manufacturerDataMask != null) {
                if (manufacturerData == null) {
                    throw new IllegalArgumentException(
                            "manufacturerData is null while manufacturerDataMask is not null");
                }
                if (manufacturerData.length() != manufacturerDataMask.length()) {
                    throw new IllegalArgumentException(
                            "size mismatch for manufacturerData and manufacturerDataMask");
                }
            }
            this.manufacturerId = manufacturerId;
            this.manufacturerData = manufacturerData;
            this.manufacturerDataMask = manufacturerDataMask;
            return this;
        }

        public ScanFilter build() {
            return new ScanFilter(deviceName, deviceAddress, serviceUuid, uuidMask,
                    serviceDataUuid, serviceData, serviceDataMask,
                    manufacturerId, manufacturerData, manufacturerDataMask);
        }
    }
}
